﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;

namespace GeoParser
{
	/// <summary>
	/// Represents a geographic point on the Earth's surface.
	/// </summary>
	public class GeoPoint : IFormattable, IEquatable<GeoPoint>
	{
		public double Latitude { get; set; }
		public double Longitude { get; set; }

		/// <summary>
		/// All this fuss just to have a correctly initialised thread-static property.
		/// </summary>
		private static readonly char d_DegreeSymbol = '*';   //'°';
		[ThreadStatic]
		private static char? m_DegreeSymbol;
		public static char DegreeSymbol
		{
			get { return m_DegreeSymbol.GetValueOrDefault(d_DegreeSymbol); }
			set { m_DegreeSymbol = value; }
		}

		protected GeoPoint()
		{
		}

		public GeoPoint(double latitude, double longitude)
		{
			Latitude = latitude;
			Longitude = longitude;
		}

		public GeoPoint(string latitude, string longitude)
		{
			Latitude = ParseDegrees(latitude);
			Longitude = ParseDegrees(longitude);
		}

		public GeoPoint(string latlong)
		{
			if (latlong == null)
				throw new ArgumentNullException("latlong");

			string[] parts = latlong.Split(new char[] {' ', ','}, StringSplitOptions.RemoveEmptyEntries);

			if (parts.Length < 2)
				throw new ArgumentException("Argument latlong must contain a space or comma as a separator");

			Latitude = ParseDegrees(parts[0]);
			Longitude = ParseDegrees(parts[1]);
		}

		public static GeoPoint Clone(GeoPoint orig)
		{
			if (orig == null)
				throw new ArgumentNullException("orig");
			return new GeoPoint(orig.Latitude, orig.Longitude);
		}

		/// <summary>
		/// Parses a string containing degrees in various potential formats.  Examples:
		/// S25*57'18.5 -25*57'18.5 -25*57.25346 -25.981234
		/// </summary>
		/// <param name="degrees">String value representing degrees latitude or longitude.</param>
		/// <exception cref="System.FormatException">Invalid number.</exception>
		/// <returns>The floating point numeric representation of the parsed value.</returns>
		public static double ParseDegrees(string degrees)
		{
			if (degrees == null)
				throw new ArgumentNullException("degrees");

			// Remove spaces
			degrees = degrees.Replace(" ", string.Empty);

			// Find any compass in the string.  Default North.
			var index = degrees.IndexOfAny(new char[] { 'N', 'S', 'E', 'W' });
			char cardinal;
			if (index >= 0)
			{
				cardinal = degrees[index];
				degrees = degrees.Remove(index, 1);
			}
			else
			{
				cardinal = 'N';
			}

			// Split up the string on the degree and minute separators
			string[] parts = degrees.Split(new char[] { DegreeSymbol, '\'' }, StringSplitOptions.RemoveEmptyEntries);

			// If there are no parts, return 0
			if (parts.Length == 0)
				return 0;

			// Read the first part as degrees
			double result = double.Parse(parts[0]);

			// Read the second part as minutes
			if (parts.Length > 1)
				result += double.Parse(parts[1]) / 60;

			// Read the third part as seconds
			if (parts.Length > 2)
				result += double.Parse(parts[2]) / 3600;

			// South and West are negative
			if (cardinal == 'S' || cardinal == 'W')
				result *= -1;

			return result;
		}

		public static Distance DistanceBetween(GeoPoint a, GeoPoint b)
		{
			if (a == null)
				throw new ArgumentNullException("a");
			if (b == null)
				throw new ArgumentNullException("b");

			if (GeoPoint.Equals(a, b))
				return new Distance();

			double theta = a.Longitude - b.Longitude;
			double dist = Math.Sin(DegreesToRadians(a.Latitude)) * Math.Sin(DegreesToRadians(b.Latitude)) +
				Math.Cos(DegreesToRadians(a.Latitude)) * Math.Cos(DegreesToRadians(b.Latitude)) * Math.Cos(DegreesToRadians(theta));

			dist = Math.Acos(dist);
			dist = RadiansToDegrees(dist);

			return new Distance(dist * 60 * 1.1515, Distance.Unit.Miles);
		}

		public static GeoPoint Average(GeoPoint a, GeoPoint b)
		{
			if (a == null)
				throw new ArgumentNullException("a");
			if (b == null)
				throw new ArgumentNullException("b");
			return new GeoPoint((a.Latitude + b.Latitude) / 2.0, (a.Longitude + b.Longitude) / 2.0);
		}

		/// <summary>
		/// Converts degrees to radians.
		/// </summary>
		/// <param name="degrees">Degree value.</param>
		/// <returns>Radian value.</returns>
		public static double DegreesToRadians(double degrees)
		{
			return (degrees * Math.PI / 180.0);
		}

		/// <summary>
		/// Converts radians to degrees.
		/// </summary>
		/// <param name="radians">Radian value.</param>
		/// <returns>Degree value.</returns>
		public static double RadiansToDegrees(double radians)
		{
			return (radians / Math.PI * 180.0);
		}

		public override string ToString()
		{
			return string.Format("{0} {1}", Latitude, Longitude);
		}

		public enum DegreeFormat
		{
			Degrees = 0,
			Minutes = 1,
			Seconds = 2,
			NumberMask = 3,
			NorthCompass = 4,
			EastCompass = 8
		}

		public virtual string ToString(DegreeFormat format)
		{
			return string.Format("{0} {1}", DegreesToString(Latitude, format), DegreesToString(Longitude, format));
		}

		public static string DegreesToString(double degrees, DegreeFormat format)
		{
			return DegreesToString(degrees, format, CultureInfo.CurrentCulture);
		}

		public static string DegreesToString(double degrees, DegreeFormat format, IFormatProvider provider)
		{
			double minutes;
			string result = string.Empty;
			switch (format & DegreeFormat.NumberMask)
			{
				case DegreeFormat.Degrees:
					result = degrees.ToString();
					break;
				case DegreeFormat.Minutes:
					minutes = Math.Abs((degrees - Math.Truncate(degrees)) * 60);
					result = string.Format(provider, "{0}{1}{2}", Math.Truncate(degrees), DegreeSymbol, minutes);
					break;
				case DegreeFormat.Seconds:
					minutes = Math.Abs((degrees - Math.Truncate(degrees)) * 60);
					double seconds = (minutes - Math.Truncate(minutes)) * 60;
					result = string.Format(provider, "{0}{1}{2}'{3}", Math.Truncate(degrees), DegreeSymbol, Math.Truncate(minutes), seconds);
					break;
			}

			if (string.IsNullOrEmpty(result))
				return string.Empty;

			return AddCompass(format, provider, result);
		}

		private static string AddCompass(DegreeFormat format, IFormatProvider provider, string degrees)
		{
			if ((format & DegreeFormat.NorthCompass) == DegreeFormat.NorthCompass)
			{
				if (degrees[0] == '-')
					return string.Format(provider, "S{0}", degrees.Substring(1));
				else
					return string.Format(provider, "N{0}", degrees);
			}
			else if ((format & DegreeFormat.EastCompass) == DegreeFormat.EastCompass)
			{
				if (degrees[0] == '-')
					return string.Format(provider, "W{0}", degrees.Substring(1));
				else
					return string.Format(provider, "E{0}", degrees);
			}
			else return degrees;
		}

		public string ToString(string format)
		{
			return ToString(format, CultureInfo.CurrentCulture);
		}

		#region IFormattable Members

		public string ToString(string format, IFormatProvider formatProvider)
		{
			DegreeFormat df = DegreeFormat.Degrees;
			if (format.IndexOf('m') > 0)
				df = DegreeFormat.Minutes;
			else if (format.IndexOf('s') > 0)
				df = DegreeFormat.Seconds;

			if (format.IndexOf('N') > 0)
				df |= DegreeFormat.NorthCompass;
			else if (format.IndexOf('E') > 0)
				df |= DegreeFormat.EastCompass;

			return ToString(df);
		}

		#endregion

		public override bool Equals(object obj)
		{
			if (obj == null)
				return false;

			if (object.ReferenceEquals(this, obj))
				return true;

			if (this.GetType() != obj.GetType())
				return false;

			return this.Equals(obj as GeoPoint);
		}

		#region IEquatable<GeoPoint> Members

		public virtual bool Equals(GeoPoint other)
		{
			return other != null && this.Latitude.Equals(other.Latitude) && this.Longitude.Equals(other.Longitude);
		}

		#endregion
	}
}